package io.netty.handler.ssl;

import java.security.PrivateKey;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLException;
import javax.net.ssl.X509ExtendedTrustManager;
import javax.net.ssl.X509TrustManager;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.netty.buffer.ByteBufAllocator;
import io.netty.internal.tcnative.SSL;
import io.netty.internal.tcnative.SSLContext;
import io.netty.internal.tcnative.SniHostNameMatcher;
import io.netty.util.CharsetUtil;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.StringUtil;

/** SSL Client Context used for sm connection */
public class SMSslServerContext extends OpenSslContext {
	private static final Logger logger = LoggerFactory.getLogger(SMSslServerContext.class.getName());

	private static final byte[] ID = { 'n', 'e', 't', 't', 'y' };
	private final OpenSslSessionContext sessionContext;

	public SMSslServerContext(X509Certificate[] trustCerts, X509Certificate[] encryptNodeCerts,
			PrivateKey encryptNodeKey, X509Certificate[] nodeCerts, PrivateKey nodeKey, ApplicationProtocolConfig apc)
			throws SSLException {
		super(null, IdentityCipherSuiteFilter.INSTANCE, apc, 0, 0, io.netty.internal.tcnative.SSL.SSL_MODE_SERVER, null,
				ClientAuth.REQUIRE, null, false, false);
		boolean success = false;
		try {
			sessionContext = newSessionContext(this, ctx, engineMap, trustCerts, encryptNodeCerts, encryptNodeKey,
					nodeCerts, nodeKey);
			success = true;
		} finally {
			if (!success) {
				release();
			}
		}
	}

	@Override
	public OpenSslSessionContext sessionContext() {
		return sessionContext;
	}

	private static final class TrustManagerVerifyCallback extends AbstractCertificateVerifier {
		private final X509TrustManager manager;

		TrustManagerVerifyCallback(OpenSslEngineMap engineMap, X509TrustManager manager) {
			super(engineMap);
			this.manager = manager;
		}

		@Override
		void verify(ReferenceCountedOpenSslEngine engine, X509Certificate[] peerCerts, String auth) throws Exception {
			manager.checkServerTrusted(peerCerts, auth);
		}
	}

	private static final class ExtendedTrustManagerVerifyCallback extends AbstractCertificateVerifier {
		private final X509ExtendedTrustManager manager;

		ExtendedTrustManagerVerifyCallback(OpenSslEngineMap engineMap, X509ExtendedTrustManager manager) {
			super(engineMap);
			this.manager = manager;
		}

		@Override
		void verify(ReferenceCountedOpenSslEngine engine, X509Certificate[] peerCerts, String auth) throws Exception {
			manager.checkServerTrusted(peerCerts, auth, engine);
		}
	}

	static void setKeyMaterial(long ctx, X509Certificate[] enNodeCert, PrivateKey enNodeKey, X509Certificate[] nodeCert,
			PrivateKey nodeKey) throws SSLException {
		long enNodeKeyBio = 0;
		long nodeKeyBio = 0;
		long enNodeCertBio = 0;
		long nodeCertBio = 0;
		PemEncoded encoded = null;
		try {
			ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
			// Only encode one time
			encoded = PemX509Certificate.toPEM(allocator, true, enNodeCert);
			enNodeCertBio = toBIO(allocator, encoded.retain());
			encoded.release();

			encoded = PemX509Certificate.toPEM(allocator, true, nodeCert);
			nodeCertBio = toBIO(allocator, encoded.retain());
			encoded.release();

			encoded = PemPrivateKey.toPEM(allocator, true, enNodeKey);
			enNodeKeyBio = toBIO(allocator, encoded.retain());
			encoded.release();

			encoded = PemPrivateKey.toPEM(allocator, true, nodeKey);
			nodeKeyBio = toBIO(allocator, encoded.retain());
			encoded.release();

			io.netty.internal.tcnative.SSLContext.setCipherSuite(ctx, "ALL", false);
			io.netty.internal.tcnative.SSLContext.setCertificateExtBio(ctx, enNodeCertBio, enNodeKeyBio, nodeCertBio,
					nodeKeyBio, StringUtil.EMPTY_STRING);
		} catch (SSLException e) {
			throw e;
		} catch (Exception e) {
			throw new SSLException("failed to set certificate and key", e);
		} finally {
			freeBio(enNodeKeyBio);
			freeBio(nodeKeyBio);
			freeBio(enNodeCertBio);
			freeBio(nodeCertBio);
			if (encoded != null && encoded.refCnt() > 0) {
				encoded.release();
			}
		}
	}

	static OpenSslServerSessionContext newSessionContext(ReferenceCountedOpenSslContext thiz, long ctx,
			OpenSslEngineMap engineMap, X509Certificate[] trustCerts, X509Certificate[] encryptNodeCerts,
			PrivateKey enNodeKey, X509Certificate[] nodeCerts, PrivateKey nodeKey) throws SSLException {
		OpenSslKeyMaterialProvider keyMaterialProvider = null;
		try {
			io.netty.internal.tcnative.SSLContext.setVerify(ctx, SSL.SSL_CVERIFY_NONE, VERIFY_DEPTH);
			setKeyMaterial(ctx, encryptNodeCerts, enNodeKey, nodeCerts, nodeKey);
		} catch (Exception e) {
			throw new SSLException("failed to set certificate and key", e);
		}

		try {
			final X509TrustManager manager = new SMTrustManager(trustCerts);

			// IMPORTANT: The callbacks set for verification must be static to prevent
			// memory leak
			// as
			// otherwise the context can never be collected. This is because the JNI code
			// holds
			// a global reference to the callbacks.
			//
			// See https://github.com/netty/netty/issues/5372

			// Use this to prevent an error when running on java < 7
			if (useExtendedTrustManager(manager)) {
				io.netty.internal.tcnative.SSLContext.setCertVerifyCallback(ctx,
						new ExtendedTrustManagerVerifyCallback(engineMap, (X509ExtendedTrustManager) manager));
			} else {
				io.netty.internal.tcnative.SSLContext.setCertVerifyCallback(ctx,
						new TrustManagerVerifyCallback(engineMap, manager));
			}

			X509Certificate[] issuers = manager.getAcceptedIssuers();
			if (issuers != null && issuers.length > 0) {
				long bio = 0;
				try {
					bio = toBIO(ByteBufAllocator.DEFAULT, issuers);
					if (!SSLContext.setCACertificateBio(ctx, bio)) {
						throw new SSLException("unable to setup accepted issuers for trustmanager " + manager);
					}
				} finally {
					freeBio(bio);
				}
			}

			if (PlatformDependent.javaVersion() >= 8) {
				// Only do on Java8+ as SNIMatcher is not supported in earlier releases.
				// IMPORTANT: The callbacks set for hostname matching must be static to prevent
				// memory leak as
				// otherwise the context can never be collected. This is because the JNI code
				// holds
				// a global reference to the matcher.
				SSLContext.setSniHostnameMatcher(ctx, new OpenSslSniHostnameMatcher(engineMap));
			}

		} catch (Exception e) {
			throw new SSLException("unable to setup TrustManager", e);
		}

		OpenSslServerSessionContext sessionContext = new OpenSslServerSessionContext(thiz, keyMaterialProvider);
		sessionContext.setSessionIdContext(ID);

		keyMaterialProvider = null;
		return sessionContext;
	}

	private static final class OpenSslSniHostnameMatcher implements SniHostNameMatcher {
		private final OpenSslEngineMap engineMap;

		OpenSslSniHostnameMatcher(OpenSslEngineMap engineMap) {
			this.engineMap = engineMap;
		}

		@Override
		public boolean match(long ssl, String hostname) {
			ReferenceCountedOpenSslEngine engine = engineMap.get(ssl);
			if (engine != null) {
				// TODO: In the next release of tcnative we should pass the byte[] directly in
				// and not use a String.
				return engine.checkSniHostnameMatch(hostname.getBytes(CharsetUtil.UTF_8));
			}
			logger.warn("No ReferenceCountedOpenSslEngine found for SSL pointer: {}", ssl);
			return false;
		}
	}
}
